package jssia.database.verification;

import static com.smoed.common.utilities.StringUtils.NEWLINE;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import jssia.database.ColumnInfo;
import jssia.database.DatabasesEnum;
import jssia.database.IndexInfo;
import jssia.database.TableInfo;
import jssia.database.mysql.MysqlTypeMap;
import jssia.database.mysql.columns.Column;
import jssia.database.mysql.columns.DecimalColumn;
import jssia.database.mysql.columns.EnumBackedColumn;
import jssia.database.mysql.columns.StringColumn;
import jssia.database.mysql.constants.Collation;
import jssia.database.mysql.constants.IndexType;
import jssia.database.mysql.preparedstatements.DbInfoStatements;
import jssia.database.mysql.transactions.TransactionManager;
import jssia.database.mysql.transactions.VoidTransaction;

import com.smoed.common.logging.Log;
import com.smoed.common.utilities.FrameworkUtils;
import com.smoed.common.utilities.StringUtils;

public final class DatabaseCreator {
    private DatabaseCreator() {
    }

    static void verifyDatabaseSchema(DatabasesEnum db)  {
        final Connection conn = db.getConnection();
        final String dbName = db.getName();
        final Collation collation = db.getCollation();

        db.executeTransaction(new VoidTransaction() {
            @Override
            public void execute() throws SQLException {
                ResultSet dbCollationResult = null;
                try {
                    dbCollationResult = DbInfoStatements.selectDatabaseCollation(conn, dbName);
                    if (dbCollationResult.next()) {
                        final String serverCollation = dbCollationResult.getString(1);
                        if (!collation.toString().equalsIgnoreCase(serverCollation)) {
                            throw new IllegalStateException("Existing server database " + dbName + " has a different collation (" + serverCollation + ") than the configured collation " + collation);
                        }
                    } else {
                        final String createDb = new StringBuilder("CREATE DATABASE IF NOT EXISTS ").append(dbName)
                            .append(" CHARACTER SET ").append(collation.getCharSet())
                            .append(" COLLATE ").append(collation).toString();

                        Statement statment = conn.createStatement();
                        statment.executeUpdate(createDb.toString());
                    }
                } finally {
                    if (dbCollationResult != null) dbCollationResult.close();
                }
            }
        });
    }

    static void createTable(DatabasesEnum db, String dbName, Class<?> tableClass, TableInfo tableInfo, MysqlTypeMap mysqlDataTypeToColumnClassMap)  {
        final StringBuilder createTable = new StringBuilder("CREATE TABLE IF NOT EXISTS ")
            .append(dbName).append(".").append(StringUtils.toDatabaseNamingConvention(tableClass.getSimpleName())).append(" ( ").append(NEWLINE);


        final List<String> columns = new ArrayList<String>();
        final List<String> indexes = new ArrayList<String>();
        for (Field field : tableClass.getDeclaredFields()) {
            try {
                final Object object = field.get(null);
                if (object instanceof Column) {
                    final Column column = (Column) object;
                    ColumnInfo columnInfo = field.getAnnotation(ColumnInfo.class);
                    if (columnInfo == null)
                        columnInfo = FrameworkUtils.getAnnotationDefault(ColumnInfo.class);

                    columns.add(getColumnCreate(column, columnInfo, mysqlDataTypeToColumnClassMap));

                    if (!columnInfo.index().type().equals(IndexType.NONE))
                        indexes.add(getIndexCreate(columnInfo.index()));
                }
            } catch (IllegalAccessException e) {
                throw new IllegalStateException("Could not access " + field + " inside of " + tableClass, e);
            }
        }

        columns.addAll(indexes);
        createTable.append(StringUtils.delimitString(columns, "," + NEWLINE)).append(NEWLINE);


        createTable.append(") ENGINE ").append(tableInfo.engine())
            .append(" CHARACTER SET ").append(tableInfo.collation().getCharSet())
            .append(" COLLATE ").append(tableInfo.collation()).append(";\n");

        final String createTableCommand = createTable.toString();
        Log.info("\n" + createTableCommand);

        try {
            Statement statment = db.getConnection().createStatement();
            statment.executeUpdate(createTableCommand);
        } catch (SQLException e) {
            TransactionManager.printSQLException(e);
            throw new IllegalStateException("Could not create database table.");
        }
    }

    private static String getColumnCreate(Column column, ColumnInfo columnInfo, MysqlTypeMap mysqlDataTypeToColumnClassMap) {
        StringBuilder colCreate = new StringBuilder();

        final Class<? extends Column> columnClass = column.getClass();
        final String mysqlDataType = getMysqlDataType(mysqlDataTypeToColumnClassMap.getMysqlType(columnClass), column, columnClass);

        colCreate.append("`").append(column.getSimpleName()).append("` ").append(mysqlDataType);

        if (!columnInfo.nullable())
            colCreate.append(" NOT");

        colCreate.append(" NULL");

        if (columnInfo.autoIncrement())
            colCreate.append(" AUTO_INCREMENT");
        if (columnInfo.defaultValue().length() > 0) {
            colCreate.append(" DEFAULT ");
            // TODO figure out something better to deal with special cases of functions as default values
            if (columnInfo.defaultValue().toUpperCase().startsWith("CURRENT_TIMESTAMP"))
                colCreate.append(columnInfo.defaultValue());
            else
                colCreate.append("'").append(columnInfo.defaultValue()).append("'");
        }

        if (StringColumn.class.isAssignableFrom(columnClass))
            colCreate.append(" COLLATE ").append(columnInfo.collation());

        return colCreate.toString();
    }

    private static String getIndexCreate(IndexInfo indexInfo) {
        StringBuilder indexCreate = new StringBuilder();
        switch (indexInfo.type()) {
        case PRIMARY:
            indexCreate.append(IndexType.PRIMARY).append(" KEY");
            break;
        default:
            indexCreate.append(indexInfo.type()).append(" `").append(indexInfo.name()).append("`");
            break;
        }

        indexCreate.append(" USING ").append(indexInfo.structure()).append(" (")
            .append(StringUtils.delimitString(Arrays.asList(indexInfo.columns()), ", "))
            .append(")");

        return indexCreate.toString();
    }

    private static String getMysqlDataType(String mysqlType, Column col, Class<? extends Column> columnClass) {
        String dataType = mysqlType;

        if (mysqlType.equals("char") || mysqlType.equals("varchar")) {
            dataType = mysqlType + "(" + ((StringColumn) col).getMaxLength() + ")";
        } else if (columnClass.equals(DecimalColumn.class)) {
            DecimalColumn decCol = (DecimalColumn) col;
            dataType = mysqlType + "(" + decCol.getPrecision() + ", " + decCol.getScale() + ")";
        } else if (EnumBackedColumn.class.isAssignableFrom(columnClass)) {
            String values = StringUtils.delimitString(((EnumBackedColumn) col).getValues(), "','");
            dataType =  "('" + values + "')";
        }
        return dataType;
    }
}
